<?php

namespace app\common\webdav\auth;

use app\admin\model\HomeUser;
use app\common\webdav\exception\NotAuthenticated;
use Sabre\HTTP\Auth\Digest;
use think\facade\Config;
use think\facade\Log;
use think\facade\Request;
use think\facade\Session;

use Sabre\DAV;

class Tools
{
    protected $realm;
    protected $nonce;
    protected $opaque;
    protected $digestParts;
    protected $A1;
    protected $qop = Digest::QOP_AUTH;


    public function __construct()
    {
        $realm = Config::get('webdav.realm', 'uldisk');
        $this->realm = $realm;
        $this->nonce = uniqid();
        $this->opaque = md5($realm);
    }

    public  function requireLogin(): HomeUser
    {

        $username = Session::get('username');

        if (empty($username)) {
            Log::debug('start validate login');
            $username = $this->autoLogin();

            Session::set('username', $username);
        } else {
            Log::debug('use session login');
        }

        $model_user = HomeUser::where('username', $username)->autoCache('read_by_username', $username)->find();

        return $model_user;
    }

    public function autoLogin()
    {
        // $authorization

        if (!empty(Request::server('PHP_AUTH_DIGEST'))) {
            return $this->validateDigestLogin();
        } else if (!empty(Request::server('PHP_AUTH_USER'))) {
            return $this->validateBasicLogin();
        }

        throw (new NotAuthenticated('Username or password was incorrect'))->setHttpHeaders($this->getNotAuthenticatedHeader());
    }

    public function validateBasicLogin()
    {

        $username = Request::server('PHP_AUTH_USER');
        $password = Request::server('PHP_AUTH_PW');

        $model_user = HomeUser::where('username', $username)->autoCache('read_by_username', $username)->find();

        if (password($password, $model_user->password_salt) != $model_user->password) {
            throw (new NotAuthenticated('Username or password was incorrect'))->setHttpHeaders($this->getNotAuthenticatedHeader());
        }

        return $username;
    }

    public  function validateDigestLogin(): string
    {

        $this->digestParts = $this->parseDigest();

        if (empty($this->digestParts['username'])) {
            throw (new NotAuthenticated("No 'Authorization: Digest' header found. Either the client didn't send one, or the server is misconfigured"))
                ->setHttpHeaders($this->getNotAuthenticatedHeader());
        }

        $username = $this->digestParts['username'];

        $model_user = HomeUser::where('username', $username)->autoCache('read_by_username', $username)->find();

        if (empty($model_user)) {
            throw (new NotAuthenticated('User not found'))->setHttpHeaders($this->getNotAuthenticatedHeader());
        }
        $hash = $model_user->webdav_password;

        if (!is_string($hash)) {
            throw new DAV\Exception('The returned value from getDigestHash must be a string or null');
        }

        // If this was false, the password or part of the hash was incorrect.
        if (!$this->validateA1($hash)) {
            throw (new NotAuthenticated('Username or password was incorrect'))->setHttpHeaders($this->getNotAuthenticatedHeader());
        }

        return $username;
    }

    /**
     * Validates the user.
     *
     * The A1 parameter should be md5($username . ':' . $realm . ':' . $password);
     */
    public function validateA1(string $A1): bool
    {
        $this->A1 = $A1;

        return $this->validate();
    }

    /**
     * Validates the digest challenge.
     */
    protected function validate(): bool
    {
        if (!is_array($this->digestParts)) {
            return false;
        }

        $A2 = Request::method(true) . ':' . $this->digestParts['uri'];

        if ('auth-int' === $this->digestParts['qop']) {
            // Making sure we support this qop value
            if (!($this->qop & Digest::QOP_AUTHINT)) {
                return false;
            }
            // We need to add an md5 of the entire request body to the A2 part of the hash
            $body = Request::getInput();
            $A2 .= ':' . md5($body);
        } elseif (!($this->qop & Digest::QOP_AUTH)) {
            return false;
        }

        $A2 = md5($A2);

        $validResponse = md5("{$this->A1}:{$this->digestParts['nonce']}:{$this->digestParts['nc']}:{$this->digestParts['cnonce']}:{$this->digestParts['qop']}:{$A2}");

        return $this->digestParts['response'] === $validResponse;
    }


    public function getNotAuthenticatedHeader()
    {

        $qop = '';
        switch ($this->qop) {
            case Digest::QOP_AUTH:
                $qop = 'auth';
                break;
            case Digest::QOP_AUTHINT:
                $qop = 'auth-int';
                break;
            case Digest::QOP_AUTH | Digest::QOP_AUTHINT:
                $qop = 'auth,auth-int';
                break;
        }

        return  ['WWW-Authenticate' => 'Digest realm="' . $this->realm . '",qop="' . $qop . '",nonce="' . $this->nonce . '",opaque="' . $this->opaque . '"'];
    }


    public function parseDigest()
    {

        $digest = Request::header('Authorization');

        // protect against missing data
        $needed_parts = ['nonce' => 1, 'nc' => 1, 'cnonce' => 1, 'qop' => 1, 'username' => 1, 'uri' => 1, 'response' => 1];
        $data = [];

        preg_match_all('@(\w+)=(?:(?:")([^"]+)"|([^\s,$]+))@', $digest, $matches, PREG_SET_ORDER);

        foreach ($matches as $m) {
            $data[$m[1]] = $m[2] ?: $m[3];
            unset($needed_parts[$m[1]]);
        }

        return $needed_parts ? false : $data;
    }
}
